﻿using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Utilities;
using System.Security.Permissions;
using Microsoft.SharePoint.Security;
using Codeplex.SPCleanup.SPUtilities.Cleanup;
using Codeplex.SPCleanup.Library.Utils;

namespace Codeplex.SPCleanup.Job
{
    public class CleanupJob : SPJobDefinition
    {
#if(DEBUG)
        bool DeleteItems = false;
#else
        bool DeleteItems = true;
#endif
        DateTime currentTime;

        const string JobTitlePrefix = "SPCleanup Job";
        const string LagLeadTimeKeyName = "CleanupJobLagLeadTime";
        const string Email_Subject_Format = "SPCleanup Notification: Your '{0}' file has been moved to the recycle bin";
        const string Email_Body_Format = "The SPCleanup has enforced its policy for outdated list items.  Your {0} file has been moved from {1} to the recycle bin as it has been lying idle for longer than the permissible time limit.  If you feel this was done in error or you otherwise need it restored, please contact your site administrator and provide them with this information";

        /// <summary>
        /// Default constructor.
        /// </summary>
        public CleanupJob() : base() { }

        /// <summary>
        /// Required constructor.
        /// </summary>
        /// <param name="jobName"></param>
        /// <param name="webApplication"></param>
        public CleanupJob(string jobName, int lagLeadTime, SPWebService spWebService)
            : base(jobName, spWebService, null, SPJobLockType.Job)
        {
            Log.Debug("Inside CleanupJob(string jobName, int lagLeadTime, SPWebService spWebService)");

            Log.Info(string.Format("jobName {0}, lagLeadTime {1} ", jobName, lagLeadTime));
            this.Title = string.Format("{0} {1}", JobTitlePrefix, jobName);

            AddKeyToFarmPropertiesBag(LagLeadTimeKeyName, lagLeadTime.ToString());
        }

        /// <summary>
        /// Method encapsulating adding of key to farm properties bag. This method checks for existence of key
        /// and removal of the same if it exists before adding the key value pair
        /// </summary>
        /// <param name="keyName"></param>
        /// <param name="value"></param>
        private void AddKeyToFarmPropertiesBag(string keyName, string value)
        {
            if (this.Farm.Properties.ContainsKey(keyName))
            {
                this.Farm.Properties.Remove(keyName);
                this.Farm.Update();
            }

            this.Farm.Properties.Add(keyName, value);
            this.Farm.Update();
        }

        /// <summary>
        /// Called by the timer job
        /// </summary>
        /// <param name="targetInstanceId"></param>
        [SharePointPermission(SecurityAction.LinkDemand, ObjectModel=true)]
        public override void Execute(Guid targetInstanceId)
        {
            try
            {
                Log.Info("CleanupJob Started");
                StartTheCleanupProcess();
                Log.Info("CleanupJob Ended");
            }
            catch (Exception exception)
            {
                Log.Error(exception.ToString());
            }

            // call the base class method to allow it to do its own work
            base.Execute(targetInstanceId);
        }

        /// <summary>
        /// Main method starting the cleanup process
        /// </summary>
        private void StartTheCleanupProcess()
        {
            Log.Info("I live inside the farm: " + this.Farm.DisplayName);

            int lagleadTime = GetLagLeadTime();

            //Set Job's Current time
            currentTime = DateTime.Now.AddHours(lagleadTime);

            Log.Debug("Current time is: " + currentTime.ToString("G"));            

            IEnumerable<SiteDefinition> sDefns;

            SPWebApplicationCollection webAppCol = SPWebService.ContentService.WebApplications;

            if (null != webAppCol)
            {
                foreach (SPWebApplication webApplication in webAppCol)
                {
                    foreach (SPSite siteCol in webApplication.Sites)
                    {

                        sDefns = ConfigManager.GetCleanupSettings(siteCol);

                        if (null != sDefns)
                        {
                            foreach (SiteDefinition siteDefn in sDefns)
                            {
                                if (siteDefn.Enabled)
                                {
                                    foreach (Site site in siteDefn.Sites)
                                    {
                                        if (site.Enabled)
                                        {
                                            foreach (SiteList sList in site.Lists)
                                            {
                                                if (sList.Enabled)
                                                {
                                                    if (null != sList.CleanupInfo)
                                                    {
                                                        Log.Info("I have to cleanup the list " + sList.ID + " in this site " + site.ID);

                                                        if (sList.CleanupInfo.ItemTimeSpan < 0)
                                                        {
                                                            Log.Warn("Will not touch the list since timespan is negative");
                                                        }
                                                        else
                                                        {
                                                            CleanupItems(siteDefn.SiteUri.ToString(), site, sList);
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Gets the lagleadtime from the property bag if it exists. By default this method returns a lagleadtime of 0
        /// </summary>
        /// <returns>the lagleadtime</returns>
        private int GetLagLeadTime()
        {
            int lagLeadTime = 0;

            if (this.Farm.Properties.ContainsKey(LagLeadTimeKeyName))
            {
                Int32.TryParse(this.Farm.Properties[LagLeadTimeKeyName].ToString(), out lagLeadTime);

                Log.Debug(string.Format("Lagleadtime = {0}", lagLeadTime));
            }
            else
            {
                Log.Warn("Missing key in farm properties bag: " + LagLeadTimeKeyName);
            }

            return lagLeadTime;
        }

        /// <summary>
        /// Iterates through the list of items and applies the cleanupinfo properties to determine whether 
        /// its time for cleanup
        /// </summary>
        /// <param name="siteUri">site's uri</param>
        /// <param name="site">site which is equivalent to SPWeb</param>
        /// <param name="sList">List to find items for cleanup</param>
        private void CleanupItems(string siteUri, Site site, SiteList sList)
        {
            Log.Debug("Inside CleanupItems(string siteUri, Site site, SiteList sList)");

            try
            {
                using (SPSite spSite = new SPSite(siteUri))
                {
                    using (SPWeb web = spSite.OpenWeb(site.ID))
                    {
                        Log.Debug(string.Format("SPWeb Name: {0}, url: {1} and title is: {2}", web.Name, web.Url, web.Title));

                        SPList list = web.Lists[sList.ID];

                        Log.Debug(string.Format("\t Found the list to cleanup: {0} ", list.Title));

                        IList<Guid> itemsToDelete = new List<Guid>();

                        // PASS 1: Browse and Remove only the File Items inside the Library

                        // Get the Items to Search
                        SPQuery query = new SPQuery(list.DefaultView);

                        if (sList.CleanupInfo.BrowseSubfolders)
                        {
                            query.ViewAttributes = "Scope=\"Recursive\"";
                        }

                        // folder evaluation takes place after this loop
                        foreach (SPListItem item in list.GetItems(query))
                        {
                            if (null == item.Folder)
                            {
                                SPListItem fullItem = item.ParentList.GetItemByUniqueId(item.UniqueId);
                            
                                if (null != item.File)//Handle files
                                {
                                    if (IsFileSetForRemoval(sList.CleanupInfo, fullItem))
                                    {
                                        itemsToDelete.Add(item.UniqueId);
                                    }
                                }
                                else //Handle everything other than files
                                {
                                    if (IsItemPastExpiration(fullItem, sList.CleanupInfo))
                                    {
                                        itemsToDelete.Add(item.UniqueId);
                                    }
                                } 
                            }
                        }

                        HandleItemRemovals(sList.CleanupInfo, list, itemsToDelete);
                        itemsToDelete.Clear();

                        // PASS 2: Delete the SubFolders if there are no child Items
                        if (sList.CleanupInfo.DeleteSubfolders)
                        {
                            query = new SPQuery(list.DefaultView);
                            query.ViewAttributes = "Scope=\"RecursiveAll\"";

                            foreach (SPListItem item in list.GetItems(query))
                            {
                                if (null != item.Folder)//Handle folders
                                {
                                    // Check for the children
                                    if (IsFolderSetForRemoval(item))
                                    {
                                        if (IsItemPastExpiration(item, sList.CleanupInfo))
                                        {
                                            itemsToDelete.Add(item.UniqueId);
                                        }
                                    }
                                }
                            }

                            HandleItemRemovals(sList.CleanupInfo, list, itemsToDelete);
                        }
                    }
                }
            }
            catch (Exception err)
            {
                Log.Error("Exception occured in CleanupItems: " + err.ToString());
            }
        }

        /// <summary>
        /// Checks if the folder has any items underneath it. If files are present under the folder it returns false
        /// else returns true
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        private bool IsFolderSetForRemoval(SPListItem item)
        {
            Log.Debug("Inside IsFolderSetForRemoval(SPListItem item)");

            bool retval = true;

            SPQuery query = new SPQuery(item.ParentList.DefaultView);
            query.Folder = item.Web.GetFolder(item.UniqueId);
            query.ViewAttributes = "Scope=\"RecursiveAll\"";

            foreach (SPListItem currItem in item.ParentList.GetItems(query))
            {
                if (null != currItem.File)
                {
                    retval = false;
                    break;
                }
            }

            return retval;
        }

        /// <summary>
        /// Wrapper method to handle item removals and to complete any preprocessing or postprocessing steps before item removal
        /// </summary>
        /// <param name="cleanupInfo">the cleanupInfo properties object</param>
        /// <param name="list">the list in which the items belong</param>
        /// <param name="itemsToDelete">Guids of the items to delete</param>
        private void HandleItemRemovals(ListCleanupInfo cleanupInfo, SPList list, IList<Guid> itemsToDelete)
        {
            foreach (Guid itemGuid in itemsToDelete)
            {
                try
                {
                    SPListItem item = null;

                    try
                    {
                        item = list.GetItemByUniqueId(itemGuid);
                    }
                    catch (System.ArgumentException ae)
                    {
                        Log.Error(string.Format("Exception occured while finding the item {0} with itemGuid during HandleItemRemovals. Exception {1}", itemGuid, ae.ToString()));
                    }

                    if (null != item)
                    {
                        Log.Info("Will delete item: " + item.Name);

                        if (null != item.File)
                        {
                            if (cleanupInfo.EmailOwner)
                            {
                                SendEmailToItemOwner(item);
                            }
                        }

                        if (DeleteItems)
                        {
                            if (null != item.File)
                            {
                                if (item.File.CheckOutStatus != SPFile.SPCheckOutStatus.None)
                                {
                                    item.File.CheckIn("File set for delete");
                                }
                            }

                            item.Recycle();
                        }
                    }
                }
                catch (Exception e)
                {
                    Log.Error(string.Format("Exception thrown during handling of item removal : {0}. Exception is {1}", itemGuid, e.ToString()));
                }
            }
        }

        /// <summary>
        /// Method handles all the rules pertaining to file removal
        /// </summary>
        /// <param name="cleanupInfo">The CleanupInfo properties object</param>
        /// <param name="item">item(file) to examine</param>
        private bool IsFileSetForRemoval(ListCleanupInfo cleanupInfo, SPListItem item)
        {
            Log.Debug("Inside IsFileSetForRemoval(ListCleanupInfo cleanupInfo, SPListItem item)");

            bool ret = false;

            if (null != item
                && null != cleanupInfo
                && null != item.File)
            {
                if (!IsFileOwnerSystemAccount(item.File))
                {
                    if (IsItemPastExpiration(item, cleanupInfo))
                    {
                        ret = true;
                    }
                }
                else
                {
                    Log.Info(string.Format("File owner for {0} is system account, hence will not set the file for removal", item.File.Name));
                }
            }

            return ret;
        }

        /// <summary>
        /// This method tries to determine an item owner and sends him/her a custom email for deletion of item
        /// </summary>
        /// <param name="item">the item which is about to be deleted</param>
        private void SendEmailToItemOwner(SPListItem item)
        {
            if (null != item)
            {
                if (null != item.File)
                {
                    SPUser owner = item.File.Author;

                    if (null != owner)
                    {
                        if (!IsFileOwnerSystemAccount(item.File))
                        {
                            try
                            {
                                if (SPUtility.SendEmail(item.Web, true, false, owner.Email
                                                    , string.Format(Email_Subject_Format, item.Name)
                                                    , string.Format(Email_Body_Format, item.Name, item.Web.Site.Url + item.ParentList.DefaultView.ServerRelativeUrl)
                                                    ))
                                {
                                    Log.Info(string.Format("Email successfully sent for file '{0}' to owner '{1}'", item.File.Name, owner.Email));
                                }
                                else
                                {
                                    Log.Warn(string.Format("Error in sending email for file '{0}' to owner '{1}'", item.File.Name, owner.Email));
                                }

                            }
                            catch (Exception e)
                            {
                                Log.Error("Error occured while sending email: " + e.ToString());
                            }
                        }
                        else
                        {
                            Log.Warn(string.Format("Will not email the file '{0}' since it belongs to system account", item.File.Name));
                        }
                    }
                }
                else //if its not a file, lets get only the description
                {
                    Log.Warn("Sending email to a non-file item has not been implemented: " + item.Name);
                }
            }
        }

        /// <summary>
        /// Wrapper method to determine if an item is past expiration
        /// </summary>
        /// <param name="item">the item to examine for expiry</param>
        /// <param name="cleanupInfo">The ListCleanupInfo properties bag to compute item expiration</param>
        /// <returns></returns>
        private bool IsItemPastExpiration(SPListItem item, ListCleanupInfo cleanupInfo)
        {
            Log.Debug("Inside IsItemPastExpiration");

            bool ret = false;

            DateTime lastModified;

            if (null != item
                && null != cleanupInfo)
            {
                Log.Info(string.Format("Cleanup info details are: {0}, delete: {1},browse: {2},email: {3}", cleanupInfo.ItemTimeSpan, cleanupInfo.DeleteSubfolders, cleanupInfo.BrowseSubfolders, cleanupInfo.EmailOwner));

                if (cleanupInfo.ItemTimeSpan > 0)
                {
                    if (DateTime.TryParse(item["Modified"].ToString(), out lastModified))
                    {
                        TimeSpan cleanupTimeSpan = GetCleanupInfoTimeSpan(cleanupInfo);

                        DateTime itemLifeSpan = lastModified.Add(cleanupTimeSpan);

                        Log.Debug("Item will be removed after: " + itemLifeSpan.ToString("G"));

                        int comparisonResult = itemLifeSpan.CompareTo(currentTime);

                        Log.Debug(string.Format("Comparison Result for {0} is: {1}", item.Name, comparisonResult));

                        if (comparisonResult <= 0)
                        {
                            ret = true;
                        }
                        else
                        {
                            ret = false;
                        }
                    }
                    else
                    {
                        Log.Warn("Cannot find Modified field as DateTime for item: " + item.ToString());
                        ret = false;
                    }
                }
                else //Special case where we dont touch the item
                {
                    Log.Info("Time Span is negative");
                    ret = false;
                }
            }
            else
            {
                Log.Warn("Input arguments are null for IsItemPastExpiration(SPListItem spItem, CleanupInfo cleanupInfo) ");
                ret = false;
            }

            return ret;
        }

        /// <summary>
        /// Computes the TimeSpan object using the ListCleanupInfo object's ItemTimeSpan
        /// uses the decimal 
        /// </summary>
        /// <param name="cleanupInfo">CleanupInfo object to extract the properties</param>
        /// <returns></returns>
        private static TimeSpan GetCleanupInfoTimeSpan(ListCleanupInfo cleanupInfo)
        {
            TimeSpan cleanupTimeSpan = new TimeSpan();

            if (null != cleanupInfo)
            {
                int hours = (int)cleanupInfo.ItemTimeSpan;
                int minutes = 0;

                if (hours > 0)
                {
                    minutes = (int)((cleanupInfo.ItemTimeSpan % hours) * 100 * 3) / 5;
                }
                else if (0 == hours)
                {
                    minutes = (int)(cleanupInfo.ItemTimeSpan * 100 * 3) / 5;
                }

                cleanupTimeSpan = new TimeSpan(hours, minutes, 0);
            }

            return cleanupTimeSpan;
        }

        /// <summary>
        /// Determines if the item owner is SystemAccount
        /// </summary>
        /// <param name="fileItem">the file item for which the owner is seeked</param>
        /// <returns></returns>
        private static bool IsFileOwnerSystemAccount(SPFile fileItem)
        {
            Log.Debug("Inside IsFileOwnerSystemAccount");

            bool ret = false;

            if (null != fileItem)
            {
                SPUser owner = fileItem.Author;

                if (null != owner)
                {
                    ret = owner.Name.Contains("System Account");
                }
                else
                {
                    ret = false;
                }
            }

            return ret;
        }
    }
}
